#!/usr/bin/env python
# encoding: utf-8


"""
OpenGL 2.0 tools for python

MIT License

Copyright (c) 2010, Markus Broecker <mbrckr@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

"""
try:
	from pyglet.gl import *
	import ctypes
except ImportError:
	print "Please check the import settings for OpenGL and ctypes!"
	raise ImportError




class Framebuffer(object):
	"""An OpenGL Framebuffer Object. It may hold a single or multiple color 
	buffers and a depth buffer or texture. If you use multiple color buffers 
	and shaders, make sure to write to gl_FragqData instead of gl_FragColor!
	"""
	
	clear_color = [0, 0, 0, 1]
	
	
	def __init__(self, resolution=(512, 512), channels=GL_RGBA, data_type=GL_UNSIGNED_BYTE, colorbuffers=1, depthbuffer_mode='buffer', min_filter=GL_LINEAR):
		"""Creates a new framebuffer object and validates it. An AssertionError
		will be thrown if the fbo is incomplete. This could be of incompatible 
		format combinations or unsupported hardware. Please consult the NVidia
		paper for that.
		
		Keyword arguments:
		resolution -- The resolution of this fbo. Must be a tuple. (default: (512, 512))
		channels -- The channel type of the color buffers in OpenGL format (default: GL_RGBA)
		data_type -- The data type of the color buffers in OpenGL format (default: GL_UNSIGNED_BYTE)
		colorbuffers -- The number of color buffers to create (default: 1)
		depthbuffer_mode -- The operating mode of the depth buffer, either 'buffer' (no read access) or 'texture' (read access) (default: 'buffer')
		min_filter -- The OpenGL texture minification filater (default: GL_LINEAR) (mipmaps are currently not supported)
		"""
		
		super(Framebuffer, self).__init__()
		self.resolution = resolution
		self.min_filter = min_filter
		
		self.colorbuffer = []
		
		# create the fbo
		self.id = gl.GLuint()
		glGenFramebuffersEXT(1, ctypes.byref(self.id))
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, self.id)
		
		# create all color buffers
		for t in range(0, colorbuffers):
			tex = self._create_texture(channels, data_type, min_filter)
			glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, int(GL_COLOR_ATTACHMENT0_EXT + t), GL_TEXTURE_2D, tex, 0)
			self.colorbuffer.append(tex)

		self.depthbuffer = gl.GLuint()
		# create the depth render buffer
		if depthbuffer_mode == 'buffer':
			glGenRenderbuffersEXT(1,byref(self.depthbuffer))
			glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, self.depthbuffer )
			glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, self.resolution[0], self.resolution[1])
			
		# create a depth texture
		if depthbuffer_mode == 'texture':
			self.depthbuffer = self._create_texture(GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, min_filter)
			glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, self.depthbuffer, 0);

		# Drop renderbuffer binding
		glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0)

		status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT)
		assert status == GL_FRAMEBUFFER_COMPLETE_EXT
		
	# TODO insert __del__, free resources!
	
	def bind(self):
		"""	Saves the current viewport and binds this framebuffer for rendering.
			All rendering calls are now executed on this framebuffer. You have
			to call disable to return to backbuffer rendering or if you are
			switching to a different framebuffer object."""
		glPushAttrib(GL_VIEWPORT_BIT)
		glViewport(0, 0, self.resolution[0], self.resolution[1])
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, self.id)
		
	

	def disable(self):
		"""Disables rendering to this framebuffer and restores the viewport."""
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0)
		glPopAttrib()
		
		# TODO support mipmaps!
	

	def clear(self):
		"""Clears the framebuffer, make sure it is bound first!"""
		glClearColor(self.clear_color[0], self.clear_color[1], self.clear_color[2], self.clear_color[3])
		glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
	

	def _create_texture(self, channels, data_type, filter=GL_LINEAR):
		"""Creates an OpenGL texture for binding"""
		tex = gl.GLuint()
		glGenTextures(1, ctypes.byref(tex))
		glBindTexture(GL_TEXTURE_2D, tex)

		glTexImage2D(GL_TEXTURE_2D, 0, channels, self.resolution[0], self.resolution[1], 0, channels, data_type, None)

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter)
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter)
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
		glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE )

		#glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);

		glBindTexture(GL_TEXTURE_2D, 0)
		return tex
	

	def __repr__(self):
		return "<Framebuffer " + str(self.id) + ">"
	


class Shader(object):
	"""An OpenGL 2.0 shader representation with access method for setting
	shader parameters."""


	def __init__(self, vertex_file, fragment_file):
		"""Creates a new shader. Currently only vertex and fragment shaders are
		supported in a program and have to be supplied by external files.
		
		Keyword arguments:
		vertex_file -- File of the vertex shader
		fragment_file -- File of the fragment shader 
		"""
		super(Shader, self).__init__()

		self._vertex_file = vertex_file
		self._fragment_file = fragment_file

		self.program = glCreateProgram()
		self._create_shader(vertex_file, GL_VERTEX_SHADER)
		self._create_shader(fragment_file, GL_FRAGMENT_SHADER)

		glLinkProgram(self.program)

		temp = c_int(0)
		glGetProgramiv(self.program, GL_LINK_STATUS, byref(temp))

		if not temp:
			print "Shader " + str(self) + " linking failed!" 

			glGetProgramiv(self.program, GL_INFO_LOG_LENGTH, byref(temp))
			buffer = create_string_buffer(temp.value)

			glGetProgramInfoLog(self.program, temp, None, buffer)
			print buffer.value
			raise RuntimeError
		else:
			print str(self) + " linked successfully"

		glValidateProgram(self.program)
		glGetProgramiv(self.program, GL_VALIDATE_STATUS, byref(temp))

		if not temp:
			print str(self) + " did not validate"
			#raise RuntimeError

	def __repr__(self):
		return "<Shader " + str(self._vertex_file) + ", " + str(self._fragment_file) + ">"

	def bind(self):
		"""Binds this shader for rendering. It will be active and replace the 
		fixed function pipeline.
		"""
		glUseProgram(self.program)

	def disable(self):
		"""Disables shaders (completely) and returns to fixed function rendering.
		"""
		glUseProgram(0)


	def _create_shader(self, filename, type):
		"""Creates and compiles an OpenGL shader object and attaches it to the
		current program.
		
		Keyword arguments:
		filename -- File to read for the shader
		type -- Shader type (GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER_EXT)
		"""

		source = []
		file = open(filename, 'r')
		for line in file:
			source.append(line)

		shader = glCreateShader(type)

		count = len(source)
		if count == 0:
			return

		# the following is taken from tristan mcdonald.
		# convert the source strings into a ctypes pointer-to-char array, and upload them
        # this is deep, dark, dangerous black magick - don't try stuff like this at home!	
		src = (c_char_p* count)(*source)
		glShaderSource(shader, count, cast(pointer(src), POINTER(POINTER(c_char))), None)

		temp = c_int(0)
		glCompileShader(shader)
		glGetShaderiv(shader, GL_COMPILE_STATUS, ctypes.byref(temp))
		if not temp:
			print "Shader " + str(self) + " compilation failed!"

			glGetShaderiv(shader, GL_INFO_LOG_LENGTH, byref(temp))
			buffer = create_string_buffer(temp.value)

			glGetShaderInfoLog(shader, temp, None, buffer)
			print buffer.value

		else:
			glAttachShader(self.program, shader)
	

	def uniform_float(self, name, *vals):
		"""Sets a number of uniform float arguments in the shader
		(float - vec4)
		
		Keyword arguments:
		name -- Variable name in the shader
		vals -- 1 to 4 float values
		
		"""
		if len(vals) in range(1, 5):
			{ 1: glUniform1f,
				2: glUniform2f,
				3: glUniform3f,
				4: glUniform4f
			}[len(vals)](glGetUniformLocation(self.program, name), *vals)
	

	def uniform_integer(self, name, *vals):
		"""Sets a number of uniform integer arguments.
		
		Keyword arguments:
		name -- Variable name in the shader
		vals -- 1 to 4 integer values
		
		"""
		if len(vals) in range(1, 5):
			{ 1: glUniform1i,
				2: glUniform2i,
				3: glUniform3i,
				4: glUniform4i
			}[len(vals)](glGetUniformLocation(self.program, name), *vals)

	def uniform_matrixf(self, name, mat):
		"""Sets a 4x4 matrix variable in this shader. The matrix is a tuple or
		list of 16 values in (OpenGL) column-first order.
		
		Keyword arguments:
		name -- Variable name
		mat -- A list or tuple of 16 values describing the matrix.
		
		"""
		loc = glGetUniformLocation(self.Handle, name)
		glUniformMatrix4fv(loc, 1, False, (c_float * 16)(*mat))
	

	def uniform_texture(self, name, texture, unit=0):
		"""Binds a texture to a uniform sampler(shadow)2D variable in this
	 	shader.
		
		Keyword arguments:
		
		name -- The variable name in the shader
		texture -- The OpenGL texture id 
		unit -- The texture unit to bind this texture to (default: 0)
		"""
		glActiveTexture(GL_TEXTURE0 + unit)
		glBindTexture(GL_TEXTURE_2D, texture)

		self.uniform_integer(name, unit)
	


# TODO create an example

def main():
	window = pyglet.window.Window(512, 512)
	
	@window.event
	def on_draw():
		window.clear()
		
	pyglet.app.run()

if __name__ == '__main__':
	main()